package com.example.spider.config;

import com.example.spider.task.AbstractSpiderTask;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.ImmutableTag;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.internal.DefaultLongTaskTimer;
import org.springframework.lang.NonNull;

import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 爬虫监控指标
 * 客户端标准 micrometer：https://www.cnblogs.com/throwable/p/13257557.html
 * <p>
 * 注意：promehteus 暂时无法很好的统计P99 这类具体，仅能统计耗时区间，总耗时、最大耗时、总数。
 * <p>
 * Counter 只能设置0 或者增加（适用于展示只增不减的指标：总请求数、访客量。场景少）
 * Guage 可上下波动的瞬时数据类型（适用于展示上下波动的此刻指标：当前请求数，当前温度，当前应用数、当前线程数。场景较少）
 * <p>
 * Timer 持续时间，如耗时
 * LongTaskTimer 耗时较高的分钟级长任务，例如一个任务平时5min完成，期望超过10min预警。假设本次执行耗时2小时，用 Timer 只能2小时跑完后才能看到这个数据触发告警。
 * https://github.com/micrometer-metrics/micrometer-docs/blob/main/src/docs/concepts/long-task-timers.adoc
 *
 * @author lym
 * @see DefaultLongTaskTimer#activeTasks
 * LongTaskTimer 更占用内存，其模型是queue 遍历，非Map，爬虫场景成千上万个大任务同时执行，记时结束时需要遍历，性能差，建议新增带任务id的Map 模型
 * <p>
 * Histogram 用于测量某些值的分布情况（适用于展示上下波动的此刻指标：当前请求数，当前温度，当前应用数、当前线程数。场景较少）
 * 【性能一般、扩展较好】
 * 统计延迟在 0-10ms 之间的请求数有多少而 10-20ms 之间的请求数又有多少
 * 为"request_duration_seconds"的直方图指标，并指定了3个桶，分别对应小于1秒、小于10秒和小于1分钟的请求持续时间：
 * Histogram histogram = Histogram.build()
 * .name("request_duration_seconds")
 * .help("Request duration in seconds.")
 * .buckets(1.0, 10.0, 60.0)
 * .register();
 * histogram.observe(5.0)
 * <p>
 * Summary 更详细的【性能好、扩展差】
 * 与 Histogram 类型类似，用于表示一段时间内的数据采样结果（通常是请求持续时间或响应大小等），但它直接存储了分位数（通过客户端计算，然后展示出来），而不是通过区间来计算。
 * <p>
 * Histogram、Summary 功能用处一样，数据在客户端存储形式不一样 它们都包含了 <basename>_sum 和 <basename>_count 指标。
 * Histogram 存储了n个区间「每个区间值=[0, le]」，每个区间内的个数，需要通过 <basename>_bucket 来计算分位数。
 * 而 Summary 则直接存储了分位数的值。
 *
 *
 * 对比：https://segmentfault.com/a/1190000039156344
 * @see url
 * https://prometheus.io/docs/concepts/metric_types/
 * https://juejin.cn/post/7029627687287128072
 * https://github.com/wufeiqun/blog/blob/master/prometheus
 * <p>
 * JVM：https://grafana.com/grafana/dashboards/4701-jvm-micrometer/?spm=a2c63.p38356.0.0.217b8f81njJXL6
 * Spring BOOT： https://grafana.com/search/?term=Spring%20Boot
 * https://grafana.com/grafana/dashboards/17175-spring-boot-observability/
 * https://grafana.com/grafana/dashboards/10280-microservices-spring-boot-2-1/?spm=a2c63.p38356.0.0.217b8f81njJXL6
 * https://xie.infoq.cn/article/d7841e308ebcaba9343b630eb
 */
public class SpiderMetrics {

    /**
     * 指标名称
     */
    private static final String TAG_NAME = "name";

    /**
     * 哪个task的
     */
    private static final String TASK_TYPE = "task";

    /**
     * 处理动作
     */
    private static final String ACTION = "action";

    /**
     * 域名
     */
    private static final String TAG_DOMAIN         = "domain";
    /**
     * 异常类型
     */
    private static final String TAG_EXCEPTION_TYPE = "ex";
    private static final String TAG_RETRY = "retry";
    /**
     * repeat跳过、重试、跳过原因、丢弃、创建源头（starter / recover）
     */
    //    private static final String TAG_EXCEPTION_TYPE = "ex";

    // warn 非预期响应、starter启动失败、HTTP响应码分布、缺少 handler

    // ex

    // ipproxy 校验个数

    // ----------------------------------------------------------

    /**
     * 动作，默认标签
     */
    private final String action;
    /**
     * 指标名称前缀（应用对于线程池监控名称的定义），一般格式为 <aapId>_<moduleId>
     * 如库存服务，同步业务的线程池监控指标前缀命名 storage_sync_threads_
     * AppInfo.appId() + _spider_
     */
    private final String metricsNamePrefix;

    // -------------------------  构造 -----------------------------------
    /**
     * 队列中的待处的任务数（当前待执行的任务数）
     * key: taskName:domain
     * value: number
     */
    private final ConcurrentHashMap<String, AtomicInteger> numberMap = new ConcurrentHashMap<>();

    /**
     * 构造器
     *
     * @param metricsNamePrefix 前缀
     * @param action            线程池属于哪个模块，为了对比多个模块，shoulder 默认把模块名放在标签上。（若不比较，也推荐放在指标名中）
     */
    public SpiderMetrics(String metricsNamePrefix, String action) {
        this.metricsNamePrefix = metricsNamePrefix;
        this.action = action;
    }

    // -------------------------  gauge -----------------------------------

    private String genMetricKey(String name) {
        return metricsNamePrefix + name;
    }

    public AtomicInteger queueSize(@NonNull String taskName, @NonNull String domain) {
        return metricsNumber("queue_tasks", taskName, domain);
    }

    public AtomicInteger activeNum(@NonNull String taskName, @NonNull String domain) {
        return metricsNumber("active", taskName, domain);
    }

    public static String exDetailStr(Throwable t) {
        if (t == null) {
            return "-";
        }
        return t.getCause() == null ? exStr(t) : exStr(t) + "__" + exCauseStr(t);
    }

    private AtomicInteger metricsNumber(String metricKey, @NonNull String taskName, @NonNull String domain) {
        String key = metricKey + "#" + taskName + "#" + domain;
        AtomicInteger cacheVal = null;
        if ((cacheVal = numberMap.get(key)) != null) {
            // 直接返回，避免重复注册产生tag的compare
            return cacheVal;
        }
        return Metrics.gauge(genMetricKey(metricKey), List.of(
                        new ImmutableTag(TASK_TYPE, action),
                        new ImmutableTag(TASK_TYPE, taskName),
                        new ImmutableTag(TAG_DOMAIN, domain)
                ), numberMap.computeIfAbsent(key, k -> new AtomicInteger())
        );
    }

    // -------------------------  counter -----------------------------------

    /**
     * @deprecated 废弃？使用完成替代
     */
    public Counter exceptionCount(@NonNull String taskName, @NonNull String domain, @NonNull String exceptionType, String retryTag) {
        return Metrics.counter(genMetricKey("exceptions"),
                ACTION, action,
                TASK_TYPE, taskName,
                TAG_DOMAIN, domain,
                TAG_EXCEPTION_TYPE, exceptionType,
                TAG_RETRY, retryTag
        );
    }

    public Counter totalCount(@NonNull String taskName, @NonNull String domain) {
        return Metrics.counter(genMetricKey("total"),
                ACTION, action,
                TASK_TYPE, taskName,
                TAG_DOMAIN, domain
        );
    }

    public Counter finishedCount(@NonNull String taskName, @NonNull String domain, @NonNull String exceptionType) {
        return Metrics.counter(genMetricKey("finished"),
                ACTION, action,
                TASK_TYPE, taskName,
                TAG_DOMAIN, domain,
                TAG_EXCEPTION_TYPE, exceptionType
        );
    }
    // -------------------------  timer -----------------------------------

    public void inQueueTask(@NonNull AbstractSpiderTask<?> task) {
        queueSize(task.getClass().getSimpleName(), task.getDomain()).incrementAndGet();
        totalCount(task.getClass().getSimpleName(), task.getDomain()).increment();
    }

    public void outQueueTask(@NonNull AbstractSpiderTask<?> task) {
        queueSize(task.getClass().getSimpleName(), task.getDomain()).decrementAndGet();
    }

    public void activeTask(@NonNull AbstractSpiderTask<?> task) {
        String taskType = task.getClass().getSimpleName();
        String domain = task.getDomain();
        activeNum(taskType, domain).incrementAndGet();
    }

    /**
     * 可根据此值，统计最大、平均、90% 95% 99%、慢任务报警
     */
    public void finishedTask(@NonNull AbstractSpiderTask<?> task, Throwable throwable, String retryTag) {
        String taskType = task.getClass().getSimpleName();
        String domain = task.getDomain();
        String exStr = exDetailStr(throwable);
        // active --
        activeNum(taskType, domain).decrementAndGet();
        // finished ++
        finishedCount(taskType, domain, exStr).increment();
        // 计算时间
        taskExecuteTime(taskType, domain, exStr).record(task.getRequestCost(), TimeUnit.MILLISECONDS);
        // 异常计数 ++
        if (throwable != null) {
            exceptionCount(taskType, domain, exStr, retryTag).increment();
        }
    }

    public Timer taskExecuteTime(@NonNull String taskName, @NonNull String domain, @NonNull String exceptionType) {
        return Metrics.timer(genMetricKey("timer"),
                ACTION, action,
                TASK_TYPE, taskName,
                TAG_DOMAIN, domain,
                TAG_EXCEPTION_TYPE, exceptionType
        );
    }

    public AtomicInteger repoNum(String repoName) {
        String key = "proxyRepo_num_" + repoName;
        AtomicInteger cacheVal = null;
        if ((cacheVal = numberMap.get(key)) != null) {
            return cacheVal;
        }
        return Metrics.gauge(genMetricKey("proxyRepo_num"), List.of(
                        new ImmutableTag("name", repoName)
                ),
                numberMap.computeIfAbsent(key, k -> new AtomicInteger())
        );
    }

    public static String exStr(Throwable t) {
        return t.getClass().getSimpleName();
    }

    public static String exCauseStr(Throwable t) {
        return Optional.ofNullable(t.getCause()).map(tt -> tt.getClass().getSimpleName()).orElse("null");
    }
}
